"""
人才流失预测
思路：
1.数据收集与处理
2.特征工程
3.模型构建与训练
4.预测结果分析
5.依据预测进行模型优化
"""

# 1.导包
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder,MinMaxScaler,StandardScaler
import time
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import RandomOverSampler,SMOTE,ADASYN
from sklearn import metrics
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import classification_report,roc_auc_score
from xgboost import XGBClassifier
import joblib
import numpy as np
from sklearn.feature_selection import RFECV
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold


# 2.载入数据
data_train = pd.read_csv('../data/train.csv')
data_test = pd.read_csv('../data/test2.csv')

# 3.了解数据
# data.info()
# print(data.head())
# print(data.describe())
# print(data.columns)


# 4.分析数据
# 4.1整体人员离职率
data_train['Attrition'].value_counts()
plt.figure(figsize=(8, 8))
plt.pie([922, 178],   # 饼图
        labels=['Stay (0)', 'Leave (1)'],
        autopct='%1.1f%%',
        colors=sns.color_palette('pastel'))
plt.title('Employee Attrition Distribution')
# plt.show()
# 结论：离职人数较少，离职率为16.2%

# 有些列可以删除，因为它们没有增加任何价值:
# a.员工数量
# b.员工人数
# C.标准工作时间:每个人工作80小时
# d.18岁以上:上述所有工作的人都在18岁以上
data_train = data_train.drop(['EmployeeNumber','EmployeeNumber','Over18','StandardHours'],axis = 1)
data_test = data_test.drop(['EmployeeNumber','Over18','StandardHours'],axis=1)
#  4.2分析年龄分布与离职率的关系
sns.kdeplot(data=data_train[data_train['Attrition'] == 1], x='Age', label='Attr')
sns.kdeplot(data=data_train[data_train['Attrition'] == 0], x='Age', label='not')
plt.grid(True)
plt.legend(loc='best')
# plt.show()

# 4.3分析离职与月收入的关系
sns.kdeplot(data=data_train[data_train['Attrition'] == 1], x='MonthlyIncome', label='Attr')
sns.kdeplot(data=data_train[data_train['Attrition'] == 0], x='MonthlyIncome', label='not')
plt.grid(True)
plt.legend(loc='best')
# plt.show()

# 4.4、分析离职和受教育程度的分析
# 使用countplot方式进行分析
sns.countplot(x="Education", hue="Attrition", data=data_train)
# 结论：受教育程度为“大学”的离职人数较多，而“高中”的离职人数较少。
# 换种呈现形式
sns.countplot(x="Attrition", hue="Education", data=data_train)

# 4.5、分析离职和年龄、家庭和距离、月收入和 previously worked 的关系
# 4.5.1.离职和年龄的关系
plt.subplot(221)
sns.boxplot(x="Attrition", y="Age", data=data_train)
# 4.5.2.离职和家庭和距离之间的关系
plt.subplot(222)
sns.boxplot(x="Attrition", y="DistanceFromHome", data=data_train)
# 4.5.3.离职和月收入的关系
plt.subplot(223)
sns.boxplot(x="Attrition", y="MonthlyIncome",
            data=data_train)
# 4.5.4.离职和曾经工作公司的关系
plt.subplot(224)
sns.boxplot(x="Attrition", y="NumCompaniesWorked", data=data_train)
# plt.show()
# 结论：年龄越小的离职人数较多，而年龄较大的离职人数较少。
# 家庭距离越远的离职人数较多，而家庭距离较近的离职人数较少。
# 月收入越低的离职人数较多，而月收入较高的离职人数较少。
# 曾经工作公司越多，离职人数较多，而曾经工作公司数量较少的离职人数较少。

# 4.6、离职和婚姻状况、性别的关系
# 离职和婚姻状况分析
plt.subplot(2, 1, 1)
sns.countplot(x="Attrition", hue="MaritalStatus", data=data_train)
# 离职和性别关系
plt.subplot(2, 1, 2)
sns.countplot(x="Attrition", hue="Gender", data=data_train)
# 结论：婚姻状况为“单身”的在离职的人员中数据分布是不同于未离职的数据分布，该特征是区分性较好的特征。同时，性别中男性较女性离职的概率较高的。

# 4.7、离职和其他变量的相关性分析
sns.pairplot(data=data_train, hue="Attrition", vars=["Age", "MonthlyIncome","NumCompaniesWorked", "PercentSalaryHike"])
# 结论：离职和年龄、家庭和距离、月收入、 previously worked 的关系，月收入越低的离职人数较多，而月收入较高的离职人数较少。

# 4.8、离职和商务旅行的关系
# sns.histplot(data=data_train, x='BusinessTravel', element='step', color='purple', stat='percent')
sns.histplot(data=data_train, x='BusinessTravel', element='bars', color='purple', stat='probability')
# 商务旅行直方图
# 可以看到大多数工作需要员工出差，但很少。大约20%的工作需要经常出差，10%没有出差。
# 结论：商务旅行对离职有影响，因为那些需要出差的人离职的概率较高。


# 4.9、离职和距离的箱形图
# sns.boxplot(data=data_train, y='DistanceFromHome', color='green')
# # 离家的距离箱形图
# # 从这个箱线图中，我们可以看到，员工平均要走7公里才能到达办公室，其中75%的员工要走1到14公里才能到达办公室。让我们看看离家的距离和损耗之间是否有任何关系
# # 结论：离家的距离对离职有影响，因为那些离家较远的人离职的概率较高。
# # 1-根据百分位数划分数据
# cut_labels = ['Near', 'Reasonable', 'Far']
# cut_bins = [-1, data_train['DistanceFromHome'].quantile(0.33), data_train['DistanceFromHome'].quantile(0.67), data_train['DistanceFromHome'].max() + 1]
# data_train['DistanceGroup'] = pd.cut(data_train['DistanceFromHome'], bins=cut_bins, labels=cut_labels)
# # 2.计算每组的退出概率
# probabilities = data_train.groupby('DistanceGroup').apply(lambda group: sum(group['Attrition'] == 'Yes') / len(group)).reset_index()
# probabilities.columns = ['DistanceGroup', 'Probability']
# # 3.绘制这些概率
# plt.figure(figsize=(10, 6))
# sns.barplot(x='DistanceGroup', y='Probability', data_train=probabilities, order=cut_labels)
# plt.title('Probability of Attrition by Distance Group')
# plt.ylabel('Probability of Quitting')
# plt.xlabel('Distance from Home Group')
# plt.show()


# 4.10、教育与工作过几家公司的关系
sns.barplot(data=data_train, y='NumCompaniesWorked', x='Education', hue='Education', palette='Set3', legend=False)

# plt.show()
# 上过高中的人换工作的频率较低。然而，拥有硕士学位的人更容易换工作
# 结论：学历对离职有影响，因为那些有博士学位的人离职的概率较高。

# 4.11、年龄和月收入散点图
sns.scatterplot(data=data_train, x='Age', y='MonthlyIncome', hue='Attrition')
plt.show()
# 从散点图中，我们可以看到，随着人们年龄的增长，高薪的机会越来越多，年长的雇员往往挣得更多，然而，工资差距也在扩大。
# 结论：年龄对离职有影响，因为那些年龄小的人离职的概率较高。

# 4.12、年龄和总工作年限的散点图
sns.scatterplot(data=data_train, x='Age', y='TotalWorkingYears', hue='Attrition')
# plt.show()
# 结论：年从散点图来看，与经验丰富的同行相比，刚开始职业生涯的人通常更容易辞职

# 5.特征工程

X_train = data_train.drop('Attrition',axis=1)
y_train = data_train['Attrition']

X_test = data_test.drop(['Attrition'],axis=1)
y_test = data_test['Attrition']

# 1. 类别特征编码
def fit_transform_features(df):
    # 1. 类别特征编码
    def encode_categorical(df):
        # 无序特征用独热编码
        nominal_features = ['BusinessTravel','Department','EducationField','Gender','JobRole','MaritalStatus','OverTime']
        encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
        # 新版本的写法
        encoded = encoder.fit_transform(df[nominal_features])
        encoded_df = pd.DataFrame(encoded, columns=encoder.get_feature_names_out(nominal_features), index=df.index)
        df = df.drop(nominal_features, axis=1)
        df = pd.concat([df, encoded_df], axis=1)
        return df, encoder
    def handle_skewness(df):
        numeric_cols = df.select_dtypes(include=['float64', 'int64']).columns
        skewed_features = []
        for col in numeric_cols:
            skewness = df[col].skew()
            if abs(skewness) > 0.5:  # 设定偏态阈值
                df[col] = np.log1p(df[col])
                skewed_features.append(col)
        print(f"进行对数变换的特征: {skewed_features}")
        return df, skewed_features

    # 编码类别特征
    df, encoder = encode_categorical(df)
    # 处理偏态分布
    df, skewed_features = handle_skewness(df)
    return df, encoder, skewed_features

def transform_features(df, encoder, skewed_features):
    # 编码类别特征
    nominal_features = ['BusinessTravel','Department','EducationField','Gender','JobRole','MaritalStatus','OverTime']
    encoded = encoder.transform(df[nominal_features])
    encoded_df = pd.DataFrame(encoded, columns=encoder.get_feature_names_out(nominal_features), index=df.index)
    df = df.drop(nominal_features, axis=1)
    df = pd.concat([df, encoded_df], axis=1)
    # 处理偏态分布
    for col in skewed_features:
        df[col] = np.log1p(df[col])
    return df

# 标准化
def fit_scale_features(df):
    numeric_cols = df.select_dtypes(include=['float64', 'int64']).columns
    scaler = StandardScaler()
    df[numeric_cols] = scaler.fit_transform(df[numeric_cols])
    return df, scaler, numeric_cols

def scale_features(df, scaler, numeric_cols):
    df[numeric_cols] = scaler.transform(df[numeric_cols])
    return df


# 处理样本不均
def handle_imbalance(X, y):
    smote = SMOTE(random_state=42)
    X_balanced, y_balanced = smote.fit_resample(X, y)
    return X_balanced, y_balanced

def select_features_cv(X, y, step=1, cv=5):
    estimator = LogisticRegression(max_iter=1000, random_state=42)
    rfecv = RFECV(
        estimator=estimator,
        step=step,
        cv=StratifiedKFold(cv),
        scoring='accuracy',
        n_jobs=-1
    )
    X_selected = rfecv.fit_transform(X, y)
    print(X_selected)
    selected_features = X.columns[rfecv.support_]
    return X_selected, selected_features, rfecv

# le = LabelEncoder()
# y_train = le.fit_transform(y_train)
# y_test = le.fit_transform(y_test)
#
# # 对非数值特征进行编码
# for col in X_train.columns:
#     if X_train[col].dtype == object:
#         X_train[col] = le.fit_transform(X_train[col])
# for col in X_test.columns:
#     if X_test[col].dtype == object:
#         X_test[col] = le.fit_transform(X_test[col])
# # 实例化ADASYN算法，设置随机种子以保证结果的可重复性
# adasyn = ADASYN(random_state=42)
# # 使用ADASYN算法对训练数据进行重采样，以解决类别不平衡问题
# # 这一步会生成合成样本，增加少数类样本的数量，帮助模型更好地学习少数类的特征
# X_train,y_train = adasyn.fit_resample(X_train,y_train)
# # 标准化数值特征
# scaler = StandardScaler()
# # 特征编码和处理偏态分布
# X_train = pd.DataFrame(scaler.fit_transform(X_train),columns=X_train.columns)
# X_test = pd.DataFrame(scaler.transform(X_test),columns=X_test.columns)
# # print(X_train)

# 在训练集上进行特征转换
start_time = time.time()
# 1. 特征编码和处理偏态分布
X_train, encoder, skewed_features = fit_transform_features(X_train)

# 2. 标准化数值特征
X_train, scaler, numeric_cols = fit_scale_features(X_train)

# 3. 处理类别不平衡（SMOTE）
X_train_balanced, y_train_balanced = handle_imbalance(X_train, y_train)

# 4. 特征选择
X_train_selected, selected_features, rfecv = select_features_cv(X_train_balanced, y_train_balanced)

# 在测试集上应用相同的转换
# 1. 特征编码和处理偏态分布
X_test = transform_features(X_test, encoder, skewed_features)

# 2. 标准化数值特征
X_test = scale_features(X_test, scaler, numeric_cols)

# 3. 特征选择
X_test_selected = X_test[selected_features]


# 6.模型训练
# 6.1.逻辑回归
lo = LogisticRegression(max_iter=1000, random_state=42)
lo = lo.fit(X_train,y_train)
y_pred1 = lo.predict(X_test)
# 打印训练时间
end_time = time.time()
execution_time = end_time - start_time
print(f"Execution time: {execution_time} seconds")
# 模型评估
metrics.confusion_matrix(y_test,y_pred1)
sns.heatmap(metrics.confusion_matrix(y_test,y_pred1),annot=True)
plt.show()
print(f'Logistic acc:{metrics.accuracy_score(y_test,y_pred1)}')
print(classification_report(y_test,y_pred1))
# 如果需要，计算 ROC AUC 分数
y_pred_proba1 = lo.predict_proba(X_test)[:, 1]
roc_auc = roc_auc_score(y_test, y_pred_proba1)
print(f"Logistic ROC AUC Score: {roc_auc:.4f}")
# 模型保存
joblib.dump(lo,"../model/lo.pkl")
# 6.2.XGBOOST
xgb = XGBClassifier(
    n_estimators=100,        # 树的数量
    learning_rate=0.1,       # 学习率
    max_depth=6,             # 树的最大深度
    random_state=42,         # 随机种子
    use_label_encoder=False, # 新版本中不需要 label encoder
    eval_metric='logloss'
)
xgb.fit(X_train,y_train)
y_pred2 = xgb.predict(X_test)
metrics.confusion_matrix(y_test,y_pred2)
sns.heatmap(metrics.confusion_matrix(y_test,y_pred2),annot=True)
plt.show()
print(f'Logistic acc:{metrics.accuracy_score(y_test,y_pred2)}')
print(classification_report(y_test,y_pred2))
#  计算 ROC AUC
y_pred_proba2 = lo.predict_proba(X_test)[:, 1]
roc_auc2 = roc_auc_score(y_test, y_pred_proba2)
print(f"XGBOOST ROC AUC Score: {roc_auc_score(y_test, y_pred2):.4f}")
# 模型保存
joblib.dump(xgb,"../model/xgb.pkl")